<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Convert GitHub issue to markdown</title>
  <style>
  * {
    box-sizing: border-box;
  }

  body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
  }

  h1 {
    margin-bottom: 24px;
    font-size: 1.5rem;
  }

  .input-group {
    margin-bottom: 16px;
  }

  label {
    display: block;
    margin-bottom: 8px;
  }

  input {
    width: 100%;
    padding: 10px 12px;
    font-size: 16px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }

  .token-help {
    font-size: 14px;
    color: #666;
    margin-top: 4px;
  }

  .button-row {
    display: flex;
    gap: 12px;
    margin-bottom: 24px;
    flex-wrap: wrap;
  }

  button {
    background: #2ea44f;
    color: white;
    border: none;
    padding: 10px 16px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
  }

  button:hover {
    background: #2c974b;
  }

  button:disabled {
    background: #94d3a2;
    cursor: not-allowed;
  }

  .output {
    margin-top: 24px;
  }

  textarea {
    width: 100%;
    min-height: 300px;
    padding: 12px;
    font-size: 16px;
    font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
    border: 1px solid #ccc;
    border-radius: 4px;
    resize: vertical;
  }

  .error {
    background: #ffebe9;
    border: 1px solid #ff8182;
    color: #cf222e;
    padding: 10px;
    border-radius: 4px;
    margin-top: 8px;
    display: none;
  }

  .error.visible {
    display: block;
  }

  .copy-button {
    margin-top: 12px;
  }

  .loading {
    display: none;
    margin: 20px 0;
    color: #666;
  }

  .loading.visible {
    display: block;
  }

  @media (max-width: 600px) {
    body {
      padding: 16px;
    }

    h1 {
      font-size: 1.25rem;
      margin-bottom: 16px;
    }

    .button-row {
      flex-direction: column;
    }

    button {
      width: 100%;
      padding: 12px 16px;
    }

    textarea {
      min-height: 200px;
      font-size: 14px;
    }

    input {
      font-size: 16px;
    }
  }
  </style>
</head>
<body>
  <h1>Convert GitHub issue to markdown</h1>

  <div class="input-group">
    <label for="token-input">GitHub Personal Access Token (optional)</label>
    <input
      type="password"
      id="token-input"
      placeholder="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    >
    <div class="token-help">For private repos and higher rate limits. Stored in browser.</div>
  </div>

  <div class="input-group">
    <label for="issue-url">GitHub issue URL</label>
    <input
      type="text"
      id="issue-url"
      placeholder="https://github.com/owner/repo/issues/123"
    >
  </div>

  <div class="button-row">
    <button id="convert">Convert to markdown</button>
  </div>

  <div id="loading" class="loading">Loading...</div>
  <div id="error" class="error"></div>

  <div class="output">
    <textarea id="markdown-output" readonly></textarea>
    <button class="copy-button" id="copy">Copy to clipboard</button>
  </div>

<script type="module">
const urlInput = document.getElementById('issue-url')
const tokenInput = document.getElementById('token-input')
const convertButton = document.getElementById('convert')
const markdownOutput = document.getElementById('markdown-output')
const copyButton = document.getElementById('copy')
const errorElement = document.getElementById('error')
const loadingElement = document.getElementById('loading')

// Load token from localStorage on page load
const savedToken = localStorage.getItem('GITHUB_TOKEN')
if (savedToken) {
  tokenInput.value = savedToken
}

// Save token to localStorage when it changes
tokenInput.addEventListener('input', () => {
  const token = tokenInput.value.trim()
  if (token) {
    localStorage.setItem('GITHUB_TOKEN', token)
  } else {
    localStorage.removeItem('GITHUB_TOKEN')
  }
})

function parseGitHubUrl(url) {
  try {
    const urlObj = new URL(url)
    const [, owner, repo, , number] = urlObj.pathname.split('/')
    if (!owner || !repo || !number) {
      throw new Error('Invalid URL format')
    }
    return { owner, repo, number }
  } catch (e) {
    throw new Error('Invalid GitHub URL. Expected format: https://github.com/owner/repo/issues/123')
  }
}

function getRequestHeaders() {
  const headers = {
    'Accept': 'application/vnd.github.v3+json'
  }
  const token = tokenInput.value.trim()
  if (token) {
    headers['Authorization'] = `token ${token}`
  }
  return headers
}

function formatDate(dateString) {
  return new Date(dateString)
    .toISOString()
    .replace('T', ' ')
    .replace('Z', ' UTC')
}

function convertToMarkdown(issue, comments) {
  let md = `# ${issue.title}\n\n`
  md += `**State:** ${issue.state}\n`
  md += `**Created by:** @${issue.user.login}\n`
  md += `**Created at:** ${formatDate(issue.created_at)}\n\n`
  md += (issue.body || '*No description provided.*') + '\n\n'

  if (comments.length > 0) {
    md += '---\n\n'
    comments.forEach(comment => {
      md += `### Comment by @${comment.user.login} at ${formatDate(comment.created_at)}\n\n`
      md += comment.body + '\n\n'
      md += '---\n\n'
    })
  }

  return md
}

// Parse GitHub blob URLs with line numbers
// e.g., https://github.com/owner/repo/blob/commit/path/to/file.py#L10-L20
function parseGitHubBlobUrl(url) {
  const match = url.match(
    /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.+?)#L(\d+)(?:-L(\d+))?$/
  )
  if (!match) return null

  const [, owner, repo, ref, path, startLine, endLine] = match
  return {
    owner,
    repo,
    ref,
    path,
    startLine: parseInt(startLine, 10),
    endLine: endLine ? parseInt(endLine, 10) : parseInt(startLine, 10)
  }
}

// Get language tag from file extension
function getLanguageFromPath(path) {
  const ext = path.split('.').pop().toLowerCase()
  const langMap = {
    'py': 'python',
    'js': 'javascript',
    'ts': 'typescript',
    'tsx': 'typescript',
    'jsx': 'javascript',
    'rb': 'ruby',
    'rs': 'rust',
    'go': 'go',
    'java': 'java',
    'c': 'c',
    'cpp': 'cpp',
    'cc': 'cpp',
    'cxx': 'cpp',
    'h': 'c',
    'hpp': 'cpp',
    'cs': 'csharp',
    'php': 'php',
    'swift': 'swift',
    'kt': 'kotlin',
    'scala': 'scala',
    'sh': 'bash',
    'bash': 'bash',
    'zsh': 'bash',
    'fish': 'fish',
    'ps1': 'powershell',
    'sql': 'sql',
    'html': 'html',
    'htm': 'html',
    'css': 'css',
    'scss': 'scss',
    'sass': 'sass',
    'less': 'less',
    'json': 'json',
    'xml': 'xml',
    'yaml': 'yaml',
    'yml': 'yaml',
    'toml': 'toml',
    'ini': 'ini',
    'md': 'markdown',
    'markdown': 'markdown',
    'r': 'r',
    'R': 'r',
    'lua': 'lua',
    'pl': 'perl',
    'pm': 'perl',
    'ex': 'elixir',
    'exs': 'elixir',
    'erl': 'erlang',
    'hs': 'haskell',
    'ml': 'ocaml',
    'fs': 'fsharp',
    'clj': 'clojure',
    'lisp': 'lisp',
    'scm': 'scheme',
    'vim': 'vim',
    'dockerfile': 'dockerfile',
    'makefile': 'makefile',
    'cmake': 'cmake',
    'tf': 'terraform',
    'hcl': 'hcl',
    'proto': 'protobuf',
    'graphql': 'graphql',
    'gql': 'graphql'
  }
  return langMap[ext] || ''
}

// Fetch code snippet from raw.githubusercontent.com
async function fetchCodeSnippet(parsed, headers) {
  const rawUrl = `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/${parsed.ref}/${parsed.path}`

  try {
    const response = await fetch(rawUrl, { headers })
    if (!response.ok) {
      return null
    }

    const content = await response.text()
    const lines = content.split('\n')

    // Line numbers are 1-indexed, array is 0-indexed
    const startIdx = parsed.startLine - 1
    const endIdx = parsed.endLine
    const selectedLines = lines.slice(startIdx, endIdx)

    return selectedLines.join('\n')
  } catch (e) {
    return null
  }
}

// Find all GitHub blob URLs in markdown and expand them with code snippets
async function expandCodeUrls(markdown, headers) {
  // Match GitHub blob URLs with line numbers that are on their own line or in markdown links
  const urlPattern = /https:\/\/github\.com\/[^\/]+\/[^\/]+\/blob\/[^\s\)]+#L\d+(?:-L\d+)?/g
  const urls = [...new Set(markdown.match(urlPattern) || [])]

  if (urls.length === 0) {
    return markdown
  }

  // Fetch all snippets in parallel
  const snippetPromises = urls.map(async (url) => {
    const parsed = parseGitHubBlobUrl(url)
    if (!parsed) return { url, snippet: null }

    const snippet = await fetchCodeSnippet(parsed, headers)
    const lang = getLanguageFromPath(parsed.path)
    return { url, snippet, lang }
  })

  const results = await Promise.all(snippetPromises)

  // Replace each URL with URL + code block
  let expandedMarkdown = markdown
  for (const { url, snippet, lang } of results) {
    if (snippet !== null) {
      const codeBlock = `\n\n\`\`\`${lang}\n${snippet}\n\`\`\``
      // Replace URL followed by optional whitespace/newline, but only if not already followed by a code block
      const urlEscaped = url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
      const pattern = new RegExp(`(${urlEscaped})(?!\\s*\n*\`\`\`)`, 'g')
      expandedMarkdown = expandedMarkdown.replace(pattern, `$1${codeBlock}`)
    }
  }

  return expandedMarkdown
}

async function getAllPages(url, headers) {
  let allItems = []
  let nextUrl = url

  while (nextUrl) {
    const response = await fetch(nextUrl, { headers })

    if (!response.ok) {
      if (response.status === 401) {
        throw new Error('Invalid GitHub token')
      } else if (response.status === 403) {
        throw new Error('Rate limit exceeded. Try adding a GitHub token.')
      }
      throw new Error('Failed to fetch comments')
    }

    const items = await response.json()
    allItems = allItems.concat(items)

    // Check for pagination in Link header
    const link = response.headers.get('Link')
    nextUrl = null

    if (link) {
      const nextLink = link.split(',').find(s => s.includes('rel="next"'))
      if (nextLink) {
        nextUrl = nextLink.split(';')[0].trim().slice(1, -1)
      }
    }
  }

  return allItems
}

async function fetchIssueAndComments(owner, repo, number) {
  const headers = getRequestHeaders()
  const issueUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${number}`
  const commentsUrl = `${issueUrl}/comments`

  const issueResponse = await fetch(issueUrl, { headers })

  if (!issueResponse.ok) {
    if (issueResponse.status === 401) {
      throw new Error('Invalid GitHub token')
    } else if (issueResponse.status === 403) {
      throw new Error('Rate limit exceeded. Try adding a GitHub token.')
    } else if (issueResponse.status === 404) {
      throw new Error('Issue not found. It may be private - try adding a GitHub token.')
    }
    throw new Error('Failed to fetch issue')
  }

  const issue = await issueResponse.json()
  const comments = await getAllPages(commentsUrl, headers)

  return { issue, comments }
}

function showError(message) {
  errorElement.textContent = message
  errorElement.classList.add('visible')
}

function hideError() {
  errorElement.textContent = ''
  errorElement.classList.remove('visible')
}

function showLoading() {
  loadingElement.classList.add('visible')
}

function hideLoading() {
  loadingElement.classList.remove('visible')
}

async function handleConvert() {
  hideError()
  markdownOutput.value = ''
  convertButton.disabled = true
  showLoading()

  try {
    const { owner, repo, number } = parseGitHubUrl(urlInput.value)
    const { issue, comments } = await fetchIssueAndComments(owner, repo, number)
    let markdown = convertToMarkdown(issue, comments)

    // Expand GitHub code URLs with their content
    const headers = getRequestHeaders()
    markdown = await expandCodeUrls(markdown, headers)

    markdownOutput.value = markdown
  } catch (error) {
    showError(error.message)
  } finally {
    hideLoading()
    convertButton.disabled = false
  }
}

convertButton.addEventListener('click', handleConvert)

urlInput.addEventListener('keypress', (e) => {
  if (e.key === 'Enter') {
    handleConvert()
  }
})

copyButton.addEventListener('click', () => {
  markdownOutput.select()
  navigator.clipboard.writeText(markdownOutput.value).then(() => {
    const originalText = copyButton.textContent
    copyButton.textContent = 'Copied!'
    copyButton.disabled = true

    setTimeout(() => {
      copyButton.textContent = originalText
      copyButton.disabled = false
    }, 1500)
  })
})

</script>
</body>
</html>
